package com.bckj.fastboot.core.extra.relational;

import com.bckj.fastboot.core.lang.model.PageResult;
import com.bckj.fastboot.core.sql.Sql;
import com.bckj.fastboot.core.sql.SqlUtils;
import org.springframework.data.domain.Pageable;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.util.Assert;

import java.lang.reflect.InvocationTargetException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * JdbcOps
 *
 * @author wfc
 **/
@SuppressWarnings({"unchecked", "SqlSourceToSinkFlow"})
public class RelationalOps {

    private final JdbcClient jdbcClient;
    private final RelationalMappingContext mappingContext;


    public RelationalOps(JdbcClient jdbcClient, RelationalMappingContext mappingContext) {
        this.jdbcClient = jdbcClient;
        this.mappingContext = mappingContext;
    }

    public <E> int save(E entity) {
        RelationalPersistentEntity<E> persistentEntity = (RelationalPersistentEntity<E>) mappingContext.getPersistentEntity(entity.getClass());
        Assert.notNull(persistentEntity, "PersistentEntity must not be null!");
        String sql = "INSERT INTO %s (%s) VALUES (%s)";
        List<String> columns = new ArrayList<>();
        List<Object> values = new ArrayList<>();
        persistentEntity.iterator().forEachRemaining(p -> {
            columns.add(p.getColumnName());
            values.add(getValue(p, entity));
        });
        String insertColumnsPart = columns.stream().map(c -> "`" + c + "`").collect(Collectors.joining(","));
        String insertValuesPart = values.stream().map(v -> "?").collect(Collectors.joining(","));
        return jdbcClient.sql(sql.formatted(persistentEntity.getTableName(), insertColumnsPart, insertValuesPart))
                .params(values).update();
    }

    public <E> void saveBatch(Iterable<E> entities) {
        entities.forEach(this::save);
    }

    private Object getValue(RelationalPersistentProperty property, Object entity) {
        Object value;
        if (property.getGetter() != null) {
            try {
                value = property.getGetter().invoke(entity);
            } catch (IllegalAccessException | InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        } else {
            if (property.getField() != null) {
                try {
                    value = property.getField().get(entity);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            } else {
                throw new RuntimeException("No field or getter found for " + property.getName());
            }
        }
        return value;
    }

    public <E> int updateById(E entity) {
        RelationalPersistentEntity<E> persistentEntity = (RelationalPersistentEntity<E>) mappingContext.getPersistentEntity(entity.getClass());
        Assert.notNull(persistentEntity, "PersistentEntity must not be null!");
        String sql = "UPDATE %s SET %s WHERE %s = ?";
        List<String> columns = new ArrayList<>();
        List<Object> values = new ArrayList<>();
        persistentEntity.iterator().forEachRemaining(p -> {
            if (p.isIdProperty()) {
                values.add(getValue(p, entity));
            } else {
                columns.add(p.getColumnName() + " = ?");
                values.add(getValue(p, entity));
            }
        });
        String updateColumnsPart = String.join(",", columns);
        String idName = Optional.ofNullable(persistentEntity.getIdProperty()).map(RelationalPersistentProperty::getColumnName)
                .orElseThrow(() -> new RuntimeException("No id property found"));
        return jdbcClient.sql(sql.formatted(persistentEntity.getTableName(), updateColumnsPart, idName))
                .params(values).update();
    }

    public <E> void updateBatchById(Iterable<E> entities) {
        entities.forEach(this::updateById);
    }

    public int update(Query query, Update update) {
        JdbcClient.StatementSpec statement = execute(query, update);
        return statement.update();
    }

    public <E> int removeById(Object id, Class<E> entityClass) {
        RelationalPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(entityClass);
        Assert.notNull(persistentEntity, "PersistentEntity must not be null!");
        String tableName = persistentEntity.getTableName();
        String idName = Optional.ofNullable(persistentEntity.getIdProperty()).map(RelationalPersistentProperty::getColumnName)
                .orElseThrow(() -> new RuntimeException("No id property found"));
        String sql = "DELETE FROM " + tableName + " WHERE " + idName + " = ?";
        return jdbcClient.sql(sql).params(id).update();
    }

    public int remove(Query query) {
        JdbcClient.StatementSpec statement = execute(query);
        return statement.update();
    }

    public <E> E findById(Object id, Class<E> entityClass) {
        RelationalPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(entityClass);
        Assert.notNull(persistentEntity, "PersistentEntity must not be null!");
        String tableName = persistentEntity.getTableName();
        String idName = Optional.ofNullable(persistentEntity.getIdProperty()).map(RelationalPersistentProperty::getColumnName)
                .orElseThrow(() -> new RuntimeException("No id property found"));
        String sql = "SELECT * FROM " + tableName + " WHERE " + idName + " = ?";
        return jdbcClient.sql(sql).params(id).query(entityClass).single();
    }

    public <E> List<E> find(Query query, Class<E> entityClass) {
        JdbcClient.StatementSpec statement = execute(query);
        return statement.query(entityClass).list();
    }

    public long count(Query query) {
        String countSql = query.countSql();
        Sql.WhereNode wheres = query.sql().getWheres();
        return (long) jdbcClient.sql(countSql + " WHREE " + wheres)
                .params(query.criteria().params())
                .query().singleValue();
    }

    public <E> PageResult<E> page(Pageable pageable, Query query, Class<E> entityClass) {
        query.with(pageable);
        List<E> list = find(query, entityClass);
        long count;
        if (query.countSql() != null) {
            count = count(query);
        } else {
            Sql countSql = SqlUtils.toCountSql(query.sql());
            count = (long) jdbcClient.sql(countSql.toString())
                    .params(query.criteria().params())
                    .query().singleValue();
        }
        return PageResult.<E>builder().items(list).total(BigInteger.valueOf(count)).build();
    }

    public JdbcClient.StatementSpec sql(String sql) {
        return jdbcClient.sql(sql);
    }

    public JdbcClient.StatementSpec execute(Query query) {
        Sql sql = query.sql();
        Pageable pageable = query.pageable();
        if (pageable.isPaged()) {
            if (pageable.getSort().isSorted()) {
                sql.orderBy(pageable.getSort().stream().map(order -> order.getProperty() + " " + order.getDirection().name()).collect(Collectors.joining(",")));
            }
            if (pageable.getPageNumber() == 0) {

                sql.lastSql(" LIMIT " + pageable.getPageSize());
            } else {
                sql.lastSql(" LIMIT " + pageable.getPageNumber() * pageable.getPageSize() + "," + pageable.getPageSize());
            }

        }
        return jdbcClient.sql(sql.toString()).params(query.criteria().params());
    }

    public JdbcClient.StatementSpec execute(Query query, Update update) {
        String updateSql = update.toString();
        Sql sql = query.sql();
        Criteria criteria = query.criteria();
        Sql.WhereNode wheres = sql.getWheres();
        JdbcClient.StatementSpec statement = jdbcClient.sql(updateSql + " WHERE " + wheres);
        statement.params(update.params());
        statement.params(criteria.params());
        return statement;
    }

}
